Animations
视图从一个位置到另一个位置的过程可以动画化吗?比如改变父级视图的场景?
当然,你可以去看一看 matchedGeometryEffect() ,这是去年的 SwiftUI 2.0 版本引入的 API
文本字体尺寸的变化可以动画化吗?
完整问题: 我们现在有办法可以动画化文本字号的变化吗?因为尺寸和背景颜色现在能够平滑过渡,但是字体会直接跳变,中间没有插值过程。
可以去参考一下 Fruta 范例工程里的 AnimatableFontModifier,那里边用了显式的字号作为可动画数据,使用场景是主视图和详情视图上的配料卡片的平滑过渡效果。这个实现对于 Fruta 来说已经够用,毕竟用例有限。
译者:作者对于进阶动画技术也有一系列博文,其中有一篇特别提到了 AnimatableModifier ( 「Swift花园」也发布过该博客的译文 —— “SwiftUI 动画进阶 - part3: AnimatableModifier”,感兴趣的读者可以查阅)
AppKit/UIKit
SwiftUI 里有像 UIView 的 drawHierarchy 那样可以把视图画到图像中的 API 吗?
SwiftUI 并没有支持这个功能的 API,但是借助 UIHostingController
,我们可以把 SwiftUI 视图包装起来,然后在 hosting controlle
r 视图上使用 drawHierarchy
实现目标。
如何控制UIViewRepresable视图的理想尺寸?
原问题: 我要如何控制一个 UIViewRepresentable
视图的理想尺寸呢?在获取被包装视图的自动尺寸时,我遇到过不少麻烦,特别是当被包装视图是 UIStackView
的时候。关于获取合适的自动尺寸有没有推荐的方式?好让我不需要过度依赖 fixedSize
?
回答:尝试在你的视图上实现intrinsicContentSize
。
UIHostingController 可以和 AnyView 一起使用吗?
原问题:我有一个框架,它会通过 UIHostingController
传出一个 SwiftUI
视图给主 app。这个视图在内部处理了它用到的所有东西,因此所有的类型都是 internal
的。唯一的 public
方法是对外给出 UIHostingController
。 为了实现隔离维护,我是这样做的:return UIHostingController(rootView: AnyView(SchedulesView(store: store)))
。这算是 AnyView
的一种正确用法吗?
答复:是的,这个用法没问题,尤其是当 AnyView
被用于视图层级的最基础层而不是用于实现动态性。不过也有别的方式,你可以封装精确类型的 hosting controller
,比如返回一个向上转换或者自定义的协议类型:
- 以
UIViewController
的类型而不是实际的UIHostingController<..>
类型返回 - 创建一个客户端期望返回类型的精确 API,然后返回那个类型
或者,你还可以借助一个容器 UIViewController
来包裹你的 hosting controller
,这种做法带来的额外好处是,调用模块可以移除对 SwiftUI
的依赖。
为什么 UIViewRepresentable 会在 makeUIView 之后和 dismantleUIView 之前更新一次?
更新函数可能因为多种原因被调用。当 UIView
存在时,它至少会被调用一次,并且在 UIView
被废弃之前可能调用多次。所以你不能依赖这个更新调用的频率。
追加的问题:我实现了一个 UIViewDiffableRepresentable
,它遵循 Hashable
协议,会在有效的更新之后检查各项属性,以防止开销很重的逻辑触发多余的 updateUIView
调用。这个做法是否优化过度了?有没有更合理的做法?
答复:这么做的确过度优化了。框架只有在 representable
结构外的属性实际改变时才会调用 updateUIView
,你可以把更新放心地托付给它。
创建 UIViewRepresentable 的时候,让 Coordinator 持有通过 updateUIView() 传入的 UIView 是一种危险的行为吗?
这样做是安全的,你的 Coordinator
会在任何视图构建之前就被创建 —— 所以在 makeUIView
里你可以让 Coordinator
持有视图的引用。
在 SwiftUI 2 中有没有可以转换旧的 AppDelegate/SceneDelegate 生命周期的方法?
是的,你可以在 App 中使用 UIApplicationDelegateAdaptor
属性包装器,比如 UIApplicationDelegateAdaptor var myDelegate: MyAppDelegate
。
SwiftUI 会为你实例化一个 UIApplicationDelegate
并且按照 AppDelegate
的方式来回调它的方法。此外,你还可以通过 configurationForConnectingSceneSession
来返回自定义的 scene delegate
,SwiftUI
也会实例化并且按照 SceneDelegate
的方式回调它的方法。
向后兼容性
能说一说现有的 SwiftUI 代码要继承新版本特性有什么方法吗?我想要用一套代码同时支持 iOS 14 和 iOS 15。
大部分新特性不能向后发布到更早的系统版本。你可以用下面的方式来检查某个特性是否可用:
1 | if #available(iOS 15, *) { |
这也就是说,确实有一些特性可以向后发布。比如,把对集合的绑定直接传入 List
和 ForEach
,然后取回每个元素的绑定:
1 | ForEach($elements) { $element in |
这个特性可以向后发布到支持 SwiftUI 的早期版本。
WWDC21 还提到了令一个向后发布的特性,那就是 enum-like 风格。
“解密 SwiftUI” 中提到用 @ViewBuilder 来消除对 AnyView 的使用,这个方法是否只能在 iOS 15 上使用?
不是,事实上这个方法可以向后发布到任何支持 SwiftUI 的版本。
编程策略
SwiftUI 中是否存在只能用 AnyView 而不能用其他替代方案构造视图的场景?
关于能不能使用 AnyView
的问题有不少。如果你能避免使用 AnyView
,我们会建议你这么做,比方说采用 @ViewBuilder
或者泛型来传递视图。
不过,我们之所以会提供 AnyView
,是因为我们明白,的确存在某些场景是其他方式无法解决的,或者权衡之下 AnyView
是可行的。
这里有一个小规则:假如被包装的视图很少改变或者几乎不改变,那么 AnyView 肯定没问题。但如果把 AnyView 用在那些会在不同的状态之间来回切换的场景,则可能会带来性能问题。这是因为为了管理这个过程,SwiftUI 需要承担额外的负荷。
Child 和 Parent 的 body ,哪一个会先被计算?
原问题:在“解密 SwiftUI” 的 Dependency Graph
部分,视频提到了两个视图,它们都依赖于相同的依赖项,需要生成新的 body。假如其中一个是另一个的子视图,那么哪个视图的 body 会被先计算 呢?
答复:父视图会先生成 body,然后递归遍历它的所有子视图。
如果不想借助 AnyView,我们要怎样传递一个视图给 ViewModifier ?
原问题:我创建了一个给视图添加自定义模态 overlay
的 ViewModifier
,效果类似 sheet
。有没有办法以构造器参数的方式把一个视图传给这个 ViewModifier
,而不用求助于 AnyView
?我希望能把 overlay
的实际内容直接传入构造器。
答复:你可以通过实现你自己的带泛型参数的 ViewModifier
来实现这个目标,比如:struct MyModifier<C: View>: ViewModifier { ... }
,然后里面声明一个类似 var content: C
这样的属性:
这里提供一个完整的例子:
1 | struct ExampleView: View { |
Group 和 ViewBuilder 该怎么选?
原问题: Group { if whatever { … } else { … } }
和 ViewBuilder.buildBlock(pageInfo == nil ? ViewBuilder.buildEither(first: EmptyView()) : ViewBuilder.buildEither(second: renderPage) )
。这两种写法都能实现条件化构建视图,哪一种更好呢?
答复:这种场景下用 Group 更好。
追加的问题:这是出于可读性的考量?还是说一者的性能会优于另一者?
答复:主要是出于可读性 —— 通常我们不建议直接调用 result builder 的实现,而是让编译器去处理它们。
我们可以用 .id() 来保持视图的等价性吗?
原问题:如果我们在条件化构建中给视图应用了相同的 id,SwiftUI 会将它们视为相同的视图吗?
1 | var body: some View { |
答复:不,它们会是两个不同的视图。
这是因为 body 是一个隐式的 ViewBuilder
。如果你并不是用 ViewBuilder
,比如上面的代码是放在另外一个普通的属性里,那么它们将会是相同的视图。或者,你也可以这样写:
1 | var body: some View { |
追加的问题:除了 body 之外,还有其他隐式 ViewBuilder 的例子吗?
答复:是的,例如 ViewModifier 的 body 函数,view style 的 makeBody, preview providers 等等,有很多。
追加的问题:所以我们应当在 view builder 尽量避免条件化吗?
答复:当然不是。条件化存在是有原因的,只是应该避免过度使用。
有没有一些场景我们应该优先使用 Hashable 而不是 Identifiable ?
如果你只需要识别一个单一值,那么 Identifiable
就是为此而生的,这意味着只有 id 属性要求是 Hashable
,而不是整个类型。
对于样式,我要怎么实现条件化?
原问题:我们如何条件化设置不同的 modifier,比如说列表的样式?
1 | List { |
答复:SwiftUI 里的样式是静态的,不允许在运行时改变,上面的场景里分支语句会更合适。不过,你首先应该考虑是否真的有必要改变样式 —— 单一样式通常是正确的选择。
假如你正在寻找某种动态机制,可以向我们提出反馈。
对 SwiftUI 视图条件化应用 modifier 有没有某种最佳实践?
原问题:给 SwiftUI 视图条件化应用 modifier
有没有最佳实践呢?我自己实现了一个 .if modifier
,当状态变化时整个视图都会刷新
答复:可以考虑采用惰性(inert) modifier
,如果哪个 modifier
缺少惰性版本,请反馈给我们。
在使用新的 SwiftUI Table 视图时,我可以把 10 个以上的 TableColumn 以 Group 的方式组织起来吗?
是的,你当然可以这么做,就像我们对视图那那样做!
追加的问题:那我是否可以理解为: @ViewBuilder
里不再有对象数量的限制了?
答复:@ViewBuilder
今年没有变化,它可以构建的元素数量仍然是有限制的。但是 Group
和嵌套 builder
可以帮助你将很多视图组合起来。
经过测试,我发现 ForEach 和控制流语句对 TableColumnBuilder 不起作用。这真遗憾,因为我能够预见,有许多场景会有这种需求。对于这个问题,我已经提出了反馈: FB9189673 (ForEach) 和 FB9189678 (控制流)。
在 UIHostingController 中使用 Core Data 要怎么回避 AnyView 呢?
原问题:在 SwiftUI 中使用 UIHostingController
和 Core Data
时,我们要如何避免因为 environment modifier
导致视图类型变化而不得不使用 AnyView
的问题呢?代码如下:
1 | import UIKit |
答复:这个问题很棒!有一个解决方案,不过里面的方括号会多到瞎… 在 class MyHostingController: UIHostingController
中,MyView
其实并不是我们的目标类型,你需要的是 MyView().environment(.managedObjectContext, persistentContainer.viewContext)
的类型,它的完整形式是 ModifiedContent<MyView,...>
(… 部分太长了,这里省略)。
通常我的做法是拷贝这个类型,然后声明一个顶级类型别名:typealias MyModifiedView = ModifiedContent<MyView, ...>
,其中右边的类型是从错误消息中复制的。这样一来,你就可以把代码写成:class MyHostingController: UIHostingController
了。
答复推荐的解决方案在之前是可以工作的。但由于现在 swiftc 已经不再报告具体类型,而是报告 some View,所以这个办法行不通了。
有一个变通的办法是先打印出具体类型:
1 | let rootView = MyView().environment(\.managedObjectContext, persistentContainer.viewContext) |
但这样做还不够,你还得强制转换类型,否则无法通过编译:
rootView as! MyModifiedView
完整的代码如下:
1 | import UIKit |
假如 SE-309 生效并且应用于 View,届时一个 View 和一个 AnyView 在视图等价性这一点上还会有实质性的区别吗?
原问题: SE-309 使得我们可以把关联类型纳入枚举的 existential type
,假设它会涵盖 View
类型,那么到那个时候,一个 View 的 existential
和 AnyView
在视图等价性上还有区别吗?
答复:我喜欢这个提案!对于实现的细节我无法评价,但相较于 existential
,AnyView
擦除了更多信息,所以 existential
仍然会是区分的边界。
现在我们有了 task,那么是否还有应该使用 onAppear 而非 task 的场景吗?
答复(工程师 #1): onAppear()
仍然可以使用。对于之前的代码可以不需要更新。我认为 task()
提供了更宽泛的解决方案,即便对于耗时很短的同步任务来说也是(适用的),因为它让你可以在将来需要的时候升级到异步的任务。
追加的问题:这么说,新代码你总是会使用 task()
,还是说 onAppear()
仍然有它自己适用的地方?我可能会认为 onAppear
类似于被弃用了?
答复(工程师 #1):我个人会始终采用 task()
,但一些人可能会喜欢 onAppear
和 onDisppear()
的对称性。
追加的问题:那么二者有区别吗?
答复(工程师 #1): task()
会在 onDisappear
时取消异步任务,并且不会再触发新的任务。
答复(工程师 #2):通常我们会避免废弃 API,除非它们真的有害。之前提到,onAppear
相比 task
有更多的限制,所以我会建议在新代码中使用 task
。但不管怎么说, onAppear
是无害的。